/*
 * ParamRefType.java
 * 
 * Copyright 2004 Christoph Csallner and Yannis Smaragdakis.
 */
package edu.gatech.cc.cnc.params;

import static edu.gatech.cc.jcrasher.Assertions.check;
import static edu.gatech.cc.jcrasher.Assertions.notNull;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import edu.gatech.cc.cnc.sym;
import edu.gatech.cc.cnc.relations.EquivalenceRelation;
import edu.gatech.cc.jcrasher.plans.expr.Variable;
import edu.gatech.cc.jcrasher.plans.expr.literals.NullLiteral;
import edu.gatech.cc.jcrasher.plans.stmt.BlockStatement;
import edu.gatech.cc.jcrasher.plans.stmt.LocalVariableDeclarationStatement;

/**
 * Represents reference type variables or method parameters:
 * arrays, strings, null, arbitrary objects.
 * 
 * @author csallner@gatech.edu (Christoph Csallner)
 */
public abstract class ParamRefType<T> extends Param<T> {
	/* Note that constraints on the components are stored in the corresponding
	 * component wrapper, which is available via elems. */

	/* Equivalence relation on reference types that contains 
	 * each constraint (p1 == p2). */
	protected static EquivalenceRelation<ParamRefType> eq = 
		new EquivalenceRelation<ParamRefType>();
	
  /**
   * Getty
   */
  public static EquivalenceRelation<ParamRefType> getEq() {
    return eq;
  }
  
  /**
   * Generate a new solution for the constrained system previously
   * prepared via prepareSolution(ParamComplex[]).
   */
  public static void generateSolution() {
    /* Reset cached plans */
    for (ParamRefType<?> param: eq.getKeys()) {
      param.plan = null;
    }
  } 
  
  
  /**
   * Reset internal data structure.
   * To be called before parsing a new counterexample.
   */
  public static void resetEq() {
    eq = new EquivalenceRelation<ParamRefType>();
  }
  
  
  
	/* Contains each param p for which constraint (p != wrappedParam) holds.
	 * Kept redundantly in p, except (null != wrappedParam). */
	private final List<ParamRefType> neq = new Vector<ParamRefType>();			
	
	/* Type of the wrapped parameter, e.g. int[][] or Foo. */
  protected Class<T> returnTypeInitially;
	protected Class<? extends T> returnType;
  
	/* (fieldNameEsc|indexParam) --> fieldWrapper */
	protected final Map<Object,Param<?>> elems = new Hashtable<Object,Param<?>>();
		
	/* Name of the corresponding local variable in the testcase to be created. */
	protected Variable<T> planName;
	
	/* Plan how to create this param. */
	protected BlockStatement plan;	 //Cache
	
  
	/**
   *  Generate a new plan from elems, w/o side-effects. 
   */
	protected abstract BlockStatement planFromElems(Variable<T> varName); 
	
	
	
	/**
	 * Constructor
	 */
	protected ParamRefType(String s){
		super(s);
    
    /* return type set later via setType */
	}

   
	@Override
	public Class<? extends T> getType() {
		return returnType;
	}
	
	protected void setType(Class<? extends T> returnType) {
		notNull(returnType);
    
		this.returnType = returnType;
    
    if (this.returnTypeInitially==null)
      returnTypeInitially = (Class<T>) returnType;
    else
      check(returnTypeInitially.isAssignableFrom(returnType));
	}
	
	@Override
	public String getTypeName() {
		return getType().getName();
	}

//	/**
//	 * @return name of local variable.
//	 */
//	public String getPlanName() {
//		if (planName==null) {getPlan();}
//		return planName.toString();
//	}
	
	
	/* NEQ */
	public boolean neqContains(ParamRefType<?> p) {return neq.contains(p);}
	
	protected void neqAdd(ParamRefType<?> p) {
		notNull(p);
		
		neq.add(p);
	}	
	
	
	/**
	 * @return (wrapped type belongs to JDK packages)
	 */
	protected boolean isSystemClass() {
		String typeName = getType().getName();
		return typeName.startsWith("java")
				|| typeName.startsWith("javax")
				|| typeName.startsWith("com.sun")
				|| typeName.startsWith("sun");
	}	
	
	
	/*
	 * Pure part of getPlan()
	 */
	protected BlockStatement<? extends T> getPlanPure() {
		planName = block.getNextID(returnTypeInitially);  //new identifier.
		
		/* name = null */
		//ParamRefType[] thisEq = eq.getEqualValues(this).toArray(new ParamRefType[eq.getEqualValues(this).size()]);  //search thisEq		
		for (ParamRefType<?> peer: eq.getEqualValues(this)) {  //null in thisEq?
			if (peer==this) {continue;} //only compare with other values
			if (peer instanceof ParamNull) {
				return new LocalVariableDeclarationStatement<T>(
            planName, new NullLiteral(getType(), testeeType));
			}
		}
		
		/* name = otherName */
		for (ParamRefType peer: eq.getEqualValues(this)) {
			if (peer==this)
        continue; //only compare with other values
      
			Class peerType = peer.getType();  //spezialize to peer.      
			if (peerType!=null && getType().isAssignableFrom(peerType)) {
        returnType=peerType;
      }						
			if (peer.plan != null) {
				return new LocalVariableDeclarationStatement<T>(
            planName, peer.planName);
			}
		}
		
		/* name = randomPlan  //do not set fields individually */
		if (isSystemClass() || Modifier.isAbstract(getType().getModifiers())) {
			if (getType().isArray()==false) {  //T[] is abstract!
				return new LocalVariableDeclarationStatement<T>( 
				    planName, JCrasherPlanner.instance().getRandom(getType(), testeeType));
			}
		}
		
		/* plan = peerPlan  //use constraints on peer's fields */
		for (ParamRefType peer: eq.getEqualValues(this)) {  //plan with peer's constraints.
			if (peer==this) {continue;} //only compare with other values
			if ((peer.plan == null) && (peer.getElemValues().size()>0)) { //use peer's planning.
				return peer.planFromElems(planName);  //cache
			}
		}			
		
		/* plan = newInstance.setFields */
		return planFromElems(planName); //plan with own constraints.					
	}
	
	
	
	
	/**
	 * Adds <code>(escName --> Param)</code> to elems.
	 */
	public void addElem(String escName, Param<?> param) {
		elems.put(escName, param);
	}
	
	
	/** @return Param associated with escName, or null. */
	public Param<?> getElemValue(String escName) {
		notNull(escName);
		return elems.get(escName);
	}
	public Collection<Param<?>> getElemValues() {
		return elems.values();
	}	
	public Set<Object> getElemKeys() {
		return elems.keySet();
	}


	/**
	 * <code>this (EQ|NEQ) null</code> 
	 */
	@Override
	public void constrain(int relation2, String constRHS) {
		/* null is the only literal supported for constRHS. */
		if (constRHS.equals("null")) {
			switch (relation2) {
      
				case sym.EQ  : 
          eq.addTuple(this, ParamNull.getCreateSingelton());
          break;
          
				case sym.NEQ :
          neqAdd(ParamNull.getCreateSingelton());
          break;
          
				default: 
					throw new IllegalArgumentException(
							"== and != are the only relations supported for two reference type params.");		
			}
		}
	}

	
	/**
	 * <code>null (EQ|NEQ) this</code> 
	 */
	@Override
	public void constrain(String constLHS, int relation2) {
		/* All supported relations are symmetric. */
		constrain(relation2, constLHS);
	}


	/**
	 * this (EQ|NEQ) p2 
	 */
	@Override
	public void constrain(int relation2, Param<?> p2) {
		if (p2 instanceof ParamRefType) {
			ParamRefType<?> rhs = (ParamRefType<?>) p2;
			
			switch (relation2) {
				case sym.EQ  : 
					eq.addTuple(this, rhs); 
					break;
				case sym.NEQ :  //wrappedParam != rhsParam
					neqAdd(rhs);
					rhs.neqAdd(this); //add redundancy			
					break;
				default: 
					throw new IllegalArgumentException(
							"== and != are the only relations supported for two reference type params.");
			}
		}
		
		//TODO: Log constraint ignored.
	}	
  
  /**
   * @return preset plan, else assemble plan from equivalence class:
   *   name = otherName
   *   name = reflection.newInstance
   */
  @Override
  public BlockStatement<? extends T> getPlan() {  /* side-effect: Maintains a cache! */
    
    if (this.plan!=null)
      return this.plan;  //cache hit
    
    this.plan = getPlanPure();  //set cache. It also set variable.
    return this.plan;
  }
}
